<?php

namespace app\common\util;

use app\BaseModel;
use think\db\Query;
use think\facade\Log;
use app\common\exception\CatchException;

/**
 * 与核销码有关的一些工具
 */
class VerifyCodeUtil
{
    /**
     * 基于数据库模型获取批量不可重复的核销码的生成器
     * 请严格按照所需数量生成，不对应数量的话，php generator会报错
     * 当$retArray=true时，支持返回数组
     *
     * @param $model 模型实例，直接new一个就可以，如果需要更复杂的限定条件，也可以传query
     * @param $field 核销码字段名
     * @param $num 数量
     * @param $set 生成核销码使用的字符集
     * @param int $length 生成码的位数
     * @param $retArray 是否返回数组
     *
     * @return array<string>|\Closure 生成器或者生成的数组
     */
    public static function getBatchUnrepeatableCode(
        BaseModel|Query $model,
        string $field,
        int $num,
        array $set = self::NUMBER,
        int $length = 8,
        bool $retArray = false
    ): array|\Closure {
        if ($num === 0) {
            return $retArray ? [] : (fn() => yield);
        }
        $workCodes = [];
        // 直接生成对应数量的核销码
        $count = $num;
        do {
            if ($count != $num) {
                Log::info("发现 $count 个重复核销码，正在重新生成");
            }
            // 和本地生成的核销码对应，并重新生成
            $codes = ArrayUtil::repeatFn(fn() => self::getCode($set, $length), $count);
            // 用in查找数据库内已存在的核销码
            $existCodes = $model->whereIn($field, $codes)->column($field);
            // 取差集（$codes - $existCodes），把不重复的核销码扔进核销码数组
            array_push($workCodes, ...array_diff($codes, $existCodes));

            // 取交集，如果有交集就得重新获取重复的核销码，直到没有交集
            $intersect = array_intersect($existCodes, $codes);
            $count = count($intersect);
        } while ($count > 0);
        // 重复上述步骤直到没有重复

        // 生成器
        return $retArray
            ? $workCodes
            : (function () use ($workCodes) {
                foreach ($workCodes as $code) {
                    yield $code;
                }
            });
    }

    /**
     * 基于数据库模型获取不可重复的核销码
     *
     * @param BaseModel|Query $model 模型实例，直接new一个就可以，如果需要更复杂的限定条件，也可以传query
     * @param string $field 核销码字段名
     * @param array $set 使用的字符集
     * @param int $length 生成码的位数
     *
     * @return string
     */
    public static function getUnrepeatableCode(BaseModel|Query $model, string $field, array $set = self::NUMBER, int $length = 8): string
    {
        $count = 0;
        $code = '';
        do {
            $count += 1;
            if ($count > 10) {
                throw new CatchException('核销码生成次数超限, 请重试');
            }
            $code = self::getCode($set, $length);
            $model = $model->where($field, $code)->findOrEmpty();
        } while (!$model->isEmpty());
        return $code;
    }

    /**
     * 核销码字符集，去掉0、O、I、L四个易被输错或混淆的字符
     */
    const CHAR = ['1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
    /**
     * 核销码字符集，只有数字
     */
    const NUMBER = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];

    /**
     * 字符集，包含全部数字和字母
     */
    const ALL = [
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'
    ];

    /**
     * 获取随机核销码
     *
     * @param array $set 使用的字符集
     * @param int $length 生成码的位数
     *
     * @return string
     */
    public static function getCode(array $set = self::NUMBER, int $length = 8): string
    {
        $randomString = '';

        // 随机从字符集中选择字符，重复 $length 次
        for ($i = 0; $i < $length; $i++) {
            $randomCharacter = self::CHAR[array_rand($set)];
            $randomString .= $randomCharacter;
        }

        return $randomString;
    }
}
